﻿/// <reference path="../q.js" />

///
/// CrmRestKit Version 3.0
/// 
/// Credits
///     - ajax /encode logic: https://github.com/stackp/promisejs/blob/master/promise.js
///

/*globales ActiveXObject, Q, Xrm, GetGlobalContext */
( function ( exports, undefined ) {
    'use strict';

    var cache = {},
        ODATA_ENDPOINT = '/XRMServices/2011/OrganizationData.svc';

    ///
    /// consturctor
    ///
    function RestKit() {

        this.version = '3.0.0-beta';
    }

    //#region utils & context

    ///
    /// The module might be invoked in a web-resource or in a form
    ///
    RestKit.prototype._getContext = function () {

        if ( cache.ctx === undefined ) {

            if ( typeof Xrm !== 'undefined' ) {

                cache.ctx = Xrm.Page.context;
            }
            else if ( typeof GetGlobalContext !== 'undefined' ) {
                /*ignore jslint start*/
                cache.ctx = GetGlobalContext();
                /*ignore jslint end*/
            }
            else {
                throw new Error( "Context is not available." );
            }
        }

        return cache.ctx;
    };

    ///
    /// Returns the server-url based on the current dynamics crm version
    ///
    RestKit.prototype._getServerUrl = function () {

        var url = null,
            localServerUrl = null,
            context = null;

        if ( cache.serverurl === undefined ) {

            localServerUrl = window.location.protocol + '//' + window.location.host,
            context = this._getContext();

            if ( Xrm.Page.context.getClientUrl !== undefined ) {
                // since version SDK 5.0.13 
                // http://www.magnetismsolutions.com/blog/gayan-pereras-blog/2013/01/07/crm-2011-polaris-new-xrm.page-method

                url = Xrm.Page.context.getClientUrl();
            }
            else if ( context.isOutlookClient() && !context.isOutlookOnline() ) {
                url = localServerUrl;
            }
            else {
                url = context.getServerUrl();
                url = url.replace( /^(http|https):\/\/([_a-zA-Z0-9\-\.]+)(:([0-9]{1,5}))?/, localServerUrl );
                url = url.replace( /\/$/, "" );
            }

            cache.serverurl = url;
        }

        return cache.serverurl;

    };

    ///
    /// Returns the joined server-url and the endpoint-url
    ///
    RestKit.prototype._getODataPath = function () {

        if ( cache.odatapath === undefined ) {

            cache.odatapath = this._getServerUrl() + ODATA_ENDPOINT;
        }

        return cache.odatapath;
    };

    ///
    /// Encodes a url components
    ///
    RestKit.prototype._encode = function ( data ) {

        var result = '';

        if ( typeof data === 'string' ) {

            result = data;
        }
        else {

            var e = encodeURIComponent;

            for ( var key in data ) {

                if ( data.hasOwnProperty( key ) ) {

                    result += '&' + e( key ) + '=' + e( data[key] );
                }
            }
        }
        return result;
    };

    //#endregion

    //#region Factories 

    ///
    /// Returns an object that reprensts a entity-reference 
    ///
    RestKit.prototype.entityReferenceFactory = function ( id, opt_logicalName ) {

        var ref = null;

        if ( id !== undefined && id !== null ) {

            ref = {
                Id: id,
                __metadata: { type: 'Microsoft.Crm.Sdk.Data.Services.EntityReference' }
            };

            if ( opt_logicalName !== undefined && opt_logicalName !== null ) {
                ref.LogicalName = opt_logicalName;
            }
        }

        return ref;
    };

    ///
    /// Creates an object that reprensts a option-set-value
    ///
    RestKit.prototype.optionSetValueFactory = function ( value ) {

        return {
            Value: value,
            __metadata: { type: 'Microsoft.Crm.Sdk.Data.Services.OptionSetValue' }
        };
    };

    ///
    /// Returns an object that represents an money value
    ///
    RestKit.prototype.moneyValueFactory = function ( value ) {

        return {
            Value: value,
            __metadata: { type: 'Microsoft.Crm.Sdk.Data.Services.Money' }

        };
    };

    //#endregion
    
    ///
    /// Parses the ODATA date-string into a date-object
    /// All queries return a date in the format "/Date(1368688809000)/"
    /// 
    RestKit.prototype.parseODataDate = function ( value ) {

        return new Date( parseInt( value.replace( '/Date(', '' ).replace( ')/', '' ), 10 ) );
    };

    ///
    /// Returns a new XMLHttpRequest
    ///
    RestKit.prototype.getXhrInstance = function () {

        var xhr;

        if ( window.XMLHttpRequest ) {

            xhr = new XMLHttpRequest();
        }
        else if ( window.ActiveXObject ) {

            try {
                xhr = new ActiveXObject( 'Msxml2.XMLHTTP' );
            }
            catch ( e ) {

                xhr = new ActiveXObject( 'Microsoft.XMLHTTP' );
            }
        }
        return xhr;
    };

    ///
    /// Executes the ajax request
    ///
    RestKit.prototype.ajax = function ( method, url, async, opt_data, opt_headers ) {

        var dfd = new Q.defer(),
            xhr = null,
            data = opt_data || {},
            payload = this._encode( data ),
            headers = opt_headers || {};

        try {

            xhr = this.getXhrInstance();

        } catch ( e ) {
           
            dfd.reject( e );

            return dfd.promise;
        }

        if ( method === 'GET' && payload ) {
            url += '?' + payload;
            payload = null;
        }

        xhr.open( method, url, ( async === undefined ) ? true : async );
        xhr.setRequestHeader( 'Content-type', 'application/json; charset=utf-8' );
        xhr.setRequestHeader( 'Accept', 'application/json' );

        for ( var h in headers ) {

            if ( headers.hasOwnProperty( h ) ) {

                xhr.setRequestHeader( h, headers[h] );
            }
        }

        xhr.onreadystatechange = function () {

            if ( xhr.readyState === 4 ) {

                var error = ( !xhr.status ||
                           ( xhr.status < 200 || xhr.status >= 300 ) &&
                           xhr.status !== 304 );

                if ( error ) {

                    dfd.reject( xhr, xhr.status, JSON.parse(xhr.responseText) );
                }
                else {

                    dfd.resolve( JSON.parse( xhr.responseText ), xhr );
                }
            }
        };

        xhr.send( payload );

        return dfd.promise;
    };
    
    RestKit.prototype.Associate = function ( entity1Id, entity1Name, entity2Id, entity2Name, relationshipName, opt_asyn ) {
        
        var async = ( opt_asyn === undefined ) ? true : opt_asyn,
            odatapath = this._getODataPath(),
            url = odatapath + "/" + entity1Name + "Set(guid'" + entity1Id + "')/$links/" + relationshipName,
            data = window.JSON.stringify( {
                uri: odatapath + "/" + entity2Name + "Set(guid'" + entity2Id + "')"
            } );
        
        return this.ajax( 'POST', url, data, async );

    };

    RestKit.prototype.Disassociate = function ( entity1Id, entity1Name, entity2Id, relationshipName, opt_asyn ) {

        var async = ( opt_asyn === undefined ) ? true : opt_asyn,
            url = this._getODataPath() + "/" + entity1Name + "Set(guid'" + entity1Id + "')/$links/" + relationshipName + "(guid'" + entity2Id + "')",
            headers = {
                'X-HTTP-Method': 'DELETE'
            };

        return this.ajax( 'POST', url, async, headers );
    };

    ///
    /// Retrieves a single record 
    ///
    RestKit.prototype.Retrieve = function ( entityName, id, columns, opt_asyn ) {
        
        var async = ( opt_asyn === undefined ) ? true : opt_asyn,
            setName = entityName + 'Set',
            url = this._getODataPath() + "/" + setName + "(guid'" + id + "')" + "?$select=" + columns.join( ',' );

        return this.ajax( 'GET', url, async );
    };

    ///
    /// Retrievs multiuple records based on filter
    /// The max number of records returned by Odata is limited to 50, the result object contains the property 
    /// 'next' and the fn loadNext that could be used to load the addional records 
    ///
    RestKit.prototype.ByQuery = function ( entityName, columns, filter, opt_asyn ) {

        // default is 'true'
        var async = ( opt_asyn === undefined ) ? true : opt_asyn,
            setName = entityName + 'Set',
            url = null;

        // in case filter is empty 
        filter = ( filter ) ? "&$filter=" + encodeURIComponent( filter ) : '';
        url = this._getODataPath() + "/" + setName + "?$select=" + columns.join( ',' ) + filter;

        return this.ajax( 'GET', url, async );
    };

    RestKit.prototype.ByQueryUrl = function ( queryUrl, opt_asyn ) {

        var async = ( opt_asyn === undefined ) ? true : opt_asyn;

        return this.ajax( 'GET', queryUrl, async );
    };

    ///
    /// Used for joins
    ///
    RestKit.prototype.ByExpandQuery = function ( entityName, columns, expand, filter, opt_asyn ) {

        var async = ( opt_asyn === undefined ) ? true : opt_asyn,
            setName = entityName + 'Set',
            url = null;

        // in case filter is empty 
        filter = ( filter ) ? "&$filter=" + encodeURIComponent( filter ) : '';

        url = this._getODataPath() + "/" + setName + "?$select=" + columns.join( ',' ) + '&$expand=' + expand + filter;

        return this.ajax( 'GET', url, async );
    };

    ///
    /// Per default a REST query returns only 50 record. This function will load all records
    ///
    RestKit.prototype.ByQueryAll = function ( entityName, columns, filter, opt_asyn ) {

        var self = this,
            dfdAll = new Q.defer(),
            allRecords = [];

        this.ByQuery( entityName, columns, filter, opt_asyn ).then( function byQueryAllSuccess( result ) {

            // add the elements to the collection
            allRecords = allRecords.concat( result.d.results );

            if ( result.d.__next ) {

                // the success-handler will be this function
                self.ByQueryUrl( result.d.__next, opt_asyn ).then( byQueryAllSuccess, dfdAll.reject );

                // call the progressCallbacks of the promise
                dfdAll.notify( result );
            }
            else {
                dfdAll.resolve( allRecords );
            }

        }, dfdAll.reject );

        return dfdAll.promise;
    };

    ///
    /// Create a new entity-record
    ///
    RestKit.prototype.Create = function ( entityName, entityObject, opt_asyn ) {

        // default is 'true'
        var async = ( opt_asyn === undefined ) ? true : opt_asyn,
            setName = entityName + 'Set',
            data = window.JSON.stringify( entityObject ),
            url = this._getODataPath() + "/" + setName;

        // returns a promise object
        return this.ajax( 'POST', url, async, data );
    };

    ///
    /// Updates the record with the stated intance.
    /// MERGE methode does not return data
    ///
    RestKit.prototype.Update = function ( entityName, id, entityObject, opt_asyn ) {

        // default is 'true'
        var async = ( opt_asyn === undefined ) ? true : opt_asyn,
            setName = entityName + 'Set',
            url = this._getODataPath() + "/" + setName + "(guid'" + id + "')",
            data = window.JSON.stringify( entityObject ),
            headers = {
                'X-HTTP-Method': 'MERGE'
            };

        // MERGE methode does not return data
        return this.ajax( 'POST', url, async, data, headers );
    };

    ///
    /// Deletes as single record identified by the id
    ///
    RestKit.prototype.Delete = function ( entityName, id, opt_asyn ) {
        
        var async = ( opt_asyn === undefined ) ? true : opt_asyn,
            setName = entityName + 'Set',
            url = this._getODataPath() + '/' + setName + "(guid'" + id + "')",
            headers = {
                'X-HTTP-Method': 'DELETE'
            };

        return this.ajax( 'POST', url, async, null, headers );
    };

    ///
    /// Public API
    ///
    exports.CrmRestKit = new RestKit();

}( window ) );